##################################################################
# File : /lua/system/utils.lua
# Description : Global utilities for Dark Skies
# Author : GPG / Neruz
##################################################################

#####################
# LOG functions
#####################

#
# removes the \000 when passing multiple arguments, arguments are concatenated with ' '
#

local oldLOG = LOG
function LOG(...)
	local joined = (' '):join(arg)
	oldLOG(joined)
end

local oldWARN = WARN
function WARN(...)
	local joined = (' '):join(arg)
	oldWARN(joined)
end

local oldSPEW = SPEW
function SPEW(...)
	local joined = (' '):join(arg)
	oldSPEW(joined)
end

# local oldError = error
# function error(...)
# 	local joined = (' '):join(arg)
# 	oldError(joined)
# end

#####################
# Math functions
#####################

#
# clamp(t,lower,upper) returns t clamped between lower and upper bound
#
function math.clamp(t,a,b)
    return math.max(math.min(t,b),a)
end

#
# Round a number to the given decimal places.
#
function math.round(num, idp)
	return tonumber(string.format("%." .. (idp or 0) .. "f", num))
end

#####################
# String functions
#####################

#
# Generate an encrypted djb2 hash.
#
function string.hash(str)
	local function djb2Hash(str, seed, range)
		local currentHash = seed or 897
	
		local strLen = str:len()
		for n = 1, strLen do
			currentHash = 33 * currentHash + str:byte(n)
		end
	
		if not range then
			return currentHash
		end
	    currentHash = math.mod(currentHash, range)
	    return currentHash
 	end
 	return djb2Hash(str)
end

#
#
#

function string.isemail(str)
	local validEmailStruct = str:match("[A-Za-z0-9%.%%%+%-]+@[A-Za-z0-9%.%%%+%-]+%.%w%w%w?%w?")
	return validEmailStruct
end

#
# Join a list of strings.
#
function string.join(delimiter, list)
	local len = table.getn(list)
	if len == 0 then 
		return "" 
	end
	local str = list[1]
	for i = 2, len do 
		str = tostring(str) .. delimiter .. tostring(list[i])
	end
	return str
end

#
# Split a string with the given pattern.
#
function string.split(str, pattern)
	pattern = pattern or "[^%s]+"
	if pattern:len() == 0 then pattern = "[^%s]+" end
	local parts = {}
	string.gsub(str, pattern, function(a) table.insert(parts, a) end)
	return parts
end

function string.match(text, pattern)
	return string.find(text, pattern, 0)
end

function string.isascii(text)
	local _, count = string.gsub(text, '[A-Za-z0-9]', '')
	local invalidChars = {}
	local strippedStr = string.gsub(text, "([%z\194-\244][\128-\191]*)", function(invalid) table.insert(invalidChars, invalid) end)
	return count == STR_Utf8Len(text), strippedStr, invalidChars
end

#####################
# Table Funcions
#####################

#
# table.append(t1,t2) appends the contents of t2 to t1. t1 and t2 must be tables
# with integer indices
#
function table.append(t1,t2)
    if not t2 then
        return t1
    end
    local r = nil
    if t1 then
        r = table.copy(t1)
    else
        r = {}
    end
    local rn = table.getn(r)
    table.setn(r, rn + table.getn(t2))
    for k,v in t2 do
        r[rn+k] = v
    end
    return r
end

#
#
#
function table.exists(t)
	return t or {}
end

#
#
#
function table.removeByKey(t, key)
	local result = t[key]
	t[key] = nil
	return result
end

#
#
#
function table.removeByValue(t, val)
    for k, v in t do
        if v == val then
            table.remove(t, k)
            return k
        end
    end
end

#
# table.copy(t) returns a shallow copy of t.
#
function table.copy(t)
    local r = {}
    for k,v in t do
        r[k] = v
    end
    return r
end

#
# table.contains(t,val) returns the key for val if it is in t.
#
function table.find(t,val)
    for k,v in t do
        if v == val then
            return k
        end
    end
	# return nil by falling off the end
end

#
# table.subset(t1,t2) returns true iff every key/value pair in t1 is also in t2
#
function table.subset(t1,t2)
    for k,v in t1 do
        if t2[k] != v then return false end
    end
    return true
end

#
# table.equal(t1,t2) returns true iff t1 and t2 contain the same key/value pairs.
#
function table.equal(t1,t2)
    return table.subset(t1,t2) and table.subset(t2,t1)
end

#
# table.removeByValue(t,val) remove a field by value instead of by index
#
function table.removeByValue(t,val)
    for k,v in t do
        if v == val then
            table.remove(t,k)
            return
        end
    end
end
#
# table.deepcopy(t) returns a copy of t with all sub-tables also copied.
#
function table.deepcopy(t,backrefs)
    if type(t)=='table' then
        if backrefs==nil then backrefs = {} end

        local b = backrefs[t]
        if b then
            return b
        end

        local r = {}
        backrefs[t] = r
        for k,v in t do
            r[k] = table.deepcopy(v,backrefs)
        end
        return r
    else
        return t
    end
end

#
# table.merged(t1,t2) returns a table in which fields from t2 overwrite
# fields from t1. Neither t1 nor t2 is modified. The returned table may
# share structure with either t1 or t2, so it is not safe to modify.
#
# For example:
#       t1 = { x=1, y=2, sub1={z=3}, sub2={w=4} }
#       t2 = { y=5, sub1={a=6}, sub2="Fred" }
#
#       merged(t1,t2) ->
#           { x=1, y=5, sub1={a=6,z=3}, sub2="Fred" }
#
#       merged(t2, t1) ->
#           { x=1, y=2, sub1={a=6,z=3}, sub2={w=4} }
#
function table.merged(t1, t2)

    if t1==t2 then
        return t1
    end

    if type(t1)!='table' or type(t2)!='table' then
        return t2
    end

    local copied = nil
    for k,v in t2 do
        if type(v)=='table' then
            v = table.merged(t1[k], v)
        end
        if t1[k] != v then
            copied = copied or table.copy(t1)
            t1 = copied
            t1[k] = v
        end
    end

    return t1
end

#
# table.cat(t1, t2) performs a shallow "merge" of t1 and t2, where t1 and t2
# are expected to be numerically keyed (existing keys are discarded).
#
# Example:
#   table.cat({1, 2, 3}, {'A', 'House', 3.14})  ->  {1, 2, 3, 'A', 'House', 3.14}
#
function table.cat(t1, t2)

    if t1==t2 then
        return t1
    end

    if type(t1)!='table' or type(t2)!='table' then
        error('table.cat(t1, t2) : expects two tables as parameters',2)
    end

    local tRet = {}
    
    for i,v in t1 do
        table.insert(tRet, v)
    end
    
    for i,v in t2 do
        table.insert(tRet, v)
    end
    
    return tRet
end

#
# table.sorted(t, [comp]) is the same as table.sort(t, comp) except it returns
# a sorted copy of t, leaving the original unchanged.
#
# [comp] is an optional comparison function, defaulting to less-than.
#
function table.sorted(t, comp)
    local r = table.copy(t)
    table.sort(r, comp)
    return r
end

#
# sort_by(field) provides a handy comparison function for sorting
# a list of tables by some field.
#
# For example,
#       my_list={ {name="Fred", ...}, {name="Wilma", ...}, {name="Betty", ...} ... }
#
#       table.sort(my_list, sort_by 'name')
#           to get names in increasing order
#
#       table.sort(my_list, sort_down_by 'name')
#           to get names in decreasing order
#
function sort_by(field)
    return function(t1,t2)
        return t1[field] < t2[field]
    end
end

function sort_down_by(field)
    return function(t1,t2)
        return t2[field] < t1[field]
    end
end

#
# table.keys(t, [comp]) - Return a list of the keys of t, sorted.
# [comp] is an optional comparison function, defaulting to less-than.
#
function table.keys(t, comp)
    local r = {}
    for k,v in t do
        table.insert(r,k)
    end
    table.sort(r, comp)
    return r
end

#
# table.values(t) - Return a list of the values of t, in unspecified order.
#
function table.values(t)
    local r = {}
    for k,v in t do
        table.insert(r,v)
    end
    return r
end

#
# sortedpairs(t, [comp]) - Iterate over a table in key-sorted order:
#   for k,v in sortedpairs(t) do
#       print(k,v)
#   end
#
# [comp] is an optional comparison function, defaulting to less-than.
#
function sortedpairs(t, comp)
    local keys = table.keys(t, comp)
    local i=1
    return function()
        local k = keys[i]
        if k!=nil then
            i=i+1
            return k,t[k]
        end
    end
end

#
# table.getsize(t) returns actual size of a table, including string keys
#
function table.getsize(t)
    if type(t) != 'table' then return end
    local size = 0
    for k, v in t do
        size = size + 1
    end
    return size
end

#
# table.inverse(t) returns a table with keys and values from t reversed.
# e.g. table.inverse {'one','two','three'} => {one=1, two=2, three=3}
#      table.inverse {foo=17, bar=100}     => {[17]=foo, [100]=bar}
#
# If t contains duplicate values, it is unspecified which one will be returned.
#
# e.g. table.inverse {foo='x', bar='x'} => possibly {x='bar'} or {x='foo'}
#
function table.inverse(t)
    r = {}
    for k,v in t do
        r[v] = k
    end
    return r
end

#
# table.map(fn,t) returns a table with the same keys as t but with
# fn applied to each value.
#
function table.map(fn, t)
    r = {}
    for k,v in t do
        r[k] = fn(v)
    end
    return r
end

#
# table.empty(t) returns true iff t has no keys/values.
#
function table.empty(t)
    for k,v in t do
        return false
    end
    return true
end

#
# Return a random element in the table. Uses Sim's random stream if IsSim()
#
function table.random(t)
    if IsSim() then
        return t[Random(table.getn(t))]
    else
        return t[math.random(table.getn(t))]
    end
end

#####################
# Misc Funcions
#####################

#
# Generate a 10 char uid. (contains only latin alphabet chars)
#
function guid()
	local s = ''
	for i = 1, 10 do
		local r = math.random(65, 90)
		s = s .. string.char(r)
	end
	return string.lower(s)
end

#
# Convert a lua object to a python compatible object represented as string.
# Only primitives converted.
#
function LuaObjToPy(obj)
	t = type(obj)
	result = ''
	if t == 'table' then
		if getmetatable(obj).__init then
			result = obj._pythonId
		else
			local tblType = obj['__type']
			obj['__type'] = nil
			if tblType == 'tuple'  then
				result = result .. '('
				for key, val in obj do
					result = result .. LuaObjToPy(val) .. ', '
				end
				result = result .. ')'
			elseif tblType == 'list'  then
				result = result .. '['
				for key, val in obj do
					result = result .. LuaObjToPy(val) .. ', '
				end
				result = result .. ']'
			elseif tblType == 'dict' or not tblType then
				result = '{'
				for key, val in obj do
					result = result ..  LuaObjToPy(key) .. ':' .. LuaObjToPy(val) .. ', '
				end
				result = result .. '}'
			end
		end
	elseif t == 'bool' then
		if obj then
			result = 'True'
		else
			result = 'False'
		end
	elseif t == 'number' then
		result = tostring(obj)
	elseif t == 'function' then
		result = debug.getinfo(obj)['name'] or obj()
	else
		result = '"' .. tostring(obj) .. '"'
	end
	return result
end

#
# Returns Dark Skies mod path.
#
function DSPath()
   for i, mod in __active_mods do 
      if mod.uid == "ee0ef729-6df3-4c02-b506-455e6fffb45d" then 
         return mod.location 
      end 
   end
   WARN('DSPath not found')
   return '' 
end

#
# Import a Dark Skies lua file.
#
function DSimport(path)
	return import(DSPath() .. path)
end

#
#
#
function GetDSBlueprint(type, id)
	return __DSBlueprints[type][id]
end

#
# Convert a quaternion value to euler.
#
function QuaternionToEuler(quaternion)
	local qx, qy, qz, qw = unpack(quaternion)
	
	local ys = (2*qy*qw) - (2*qx*qz)
	local yc = 1 - (2 * math.pow(qy, 2)) - (2 * math.pow(qz, 2))
	local yaw = math.atan2(ys , yc)
	
	local roll = math.asin(2*qx*qy + 2*qz*qw)
	
	local ps = (2*qx*qw ) - (2*qy*qz)
	local pc = 1 - (2 * math.pow(qx, 2)) - (2 * math.pow(qz, 2))
	local pitch = math.atan2(ps, pc)
	return roll, pitch, yaw
end

#
# eval a lua statement and return it's value.
#
function dostring(str)
	return assert(loadstring('return ' .. str))()
end

#
# RandomIter(table) returns a function that when called, returns a pseudo-random element of the supplied table. 
# Each element of the table will be returned once. This is essentially for "shuffling" sets.
#
function RandomIter(someSet)
    local keyList = {}
    for key, val in someSet do
        table.insert(keyList, key)
    end
    
    return function() 
        local size = table.getn(keyList)
        
        if size > 0 then
            local key = table.remove(keyList, Random(1, size))
            return key, someSet[key]
        else
            return
        end
    end
end

#
# safecall(msg, fn, ...) calls the given function with the given
# args, and catches any error and logs a warning including the given msg.
# Returns nil if the function failed, otherwise returns the function's result.
#
function safecall(msg, fn, ...)
    local ok, result = pcall(fn, unpack(arg))
    if ok then
        return result
    else
        WARN("Problem " .. tostring(msg) .. ":\n" .. result)
        return
    end
end

#
# ParseEntityCategoryEx converts a string using natural + - * syntax into an
# entity category. Ex since the engine defines the old style.
#
function ParseEntityCategoryEx(cat)
    local result = string.gsub(cat, "(%u+)", "categories.%1")
    local f = loadstring("return " .. result)
    return f()
end

